const path = require('path');
const fs = require('fs-extra');
const compressing = require('compressing');
const moment = require('moment');
const { statusKind, getLinesOfRemoteFileStream, kind } = require('../../../files');
const functionModels = require('../../../functions');
const fileElementModels = require('../../../file-elements');
const FunctionRelation = require('../../../mongo/function-relation');
const FileDataFlow = require('../../../mongo/data-flow');
const FunctionCFG = require('../../../mongo/function-cfg');
const BaseReporter = require('../base-report');
const utility = require('../../../utility');
const ProgressIndicator = require('../../../progress-indicator');

/**
  DD（两次变量定义之间没有被使用），DU（变量定义但没有使用），UR（变量未定义就引用）
  RO(只引用) DO(只定义) DR(既定义也使用)
  R(引用) D(定义) E(声明) I(作为输入) O(作为输出)
 */
const variableCheck = {
  notUsedBetweenTwoDefined: 'DD',
  definedButNotUsed: 'DU',
  referencedBeforeDefined: 'UR',
  onlyReferenced: 'RO',
  onlyDefined: 'DO',
  bothDefineAndUsed: 'DR',
  referenced: 'R',
  defined: 'D',
  wasDeclared: 'E',
  asInput: 'I',
  asOutput: 'O',
};
// L(局部变量) G(全局变量) P(参数) LG(局部全局变量/局部静态变量)
const variableKind = {
  local: 'L',
  global: 'G',
  parameter: 'P',
  localStatic: 'LG',
};
class DataStreamReporter extends BaseReporter {
  constructor(versionId, locale, version = null, project = null, creator = null, outputDir = null, format) {
    super(versionId, locale, version, project, creator, outputDir, __dirname, format);
    this.reportName = this.i18n.__('tests.dataStreamReport');
    this.filePathMap = new Map();
    this.versionFunctionMap = new Map();
  }

  /**
     * 报告生成
     */
  generate(progressIndicator = new ProgressIndicator()) {
    const result = {
      cachePath: this.outputDir,
    };
    const [filesProgressIndicator, summaryProgressIndictor] = progressIndicator.split([0.9, 0.1]);
    return new Promise((resolve, reject) => {
      functionModels.getFunctionByVersionId(this.versionId, ['_id', 'mangledId', 'functionName'])
        .then((allFuncs) => {
          this.versionFunctionMap = allFuncs;
          return this.spliteByFile(filesProgressIndicator);
        })
        .then((filesData) => this.generateSummary(filesData, summaryProgressIndictor))
        .then(() => {
          const fileName = `${this.project.projectName}-${this.version.versionName}-${this.reportName}-${this.format}.zip`;
          result.reportPath = path.join(this.outputDir, fileName);
          const reportsDir = path.join(this.outputDir, this.project.projectName);
          return compressing.zip.compressDir(reportsDir, result.reportPath);
        })
        .then(() => {
          resolve(result);
        })
        .catch(reject);
    });
  }

  /**
   * 按文件分割
   * @param {ProgressIndicator} progressIndicator 进度指示器
   * @return {Promise}
   */
  spliteByFile(progressIndicator) {
    return new Promise((resolve, reject) => {
      this.spliteByProjectVersion((versionDir) => fs.ensureDir(versionDir))
        .then(({ versionPath }) => {
          this.versionDir = versionPath;
          if (this.reportRange && this.reportRange.indexOf(2) < 0) {
            return resolve([]);
          }
          return this.getDataAdapter().getFiles(this.versionId);
        })
        .then((resFiles) => this.handleFiles(resFiles, this.versionDir, progressIndicator))
        .then((filesData) => {
          resolve(filesData);
        })
        .catch(reject);
    });
  }

  /**
   *  处理文件中的函数调用关系
   * @param {Object} file
   */
  async handleFunctionsOfFile(file) {
    const resFileFunctions = [];
    const fileFunctions = await functionModels.getFunctions(this.versionId, { kinds: fileElementModels.getCountableKinds() }, file._id);
    const fileCfgs = await FunctionCFG.find({ fileId: file._id });
    const functionCfgCallexprMap = new Map();
    fileCfgs.forEach((cfg) => {
      functionCfgCallexprMap.set(cfg.functionId.toString(), cfg.callexpr || '');
    });
    await utility.arrayChunkOperation(fileFunctions, 50, async (func) => {
      const functionId = func._id;
      const newFunc = {
        functionName: func.functionName,
        line: func.location.line,
        filePath: file.path,
        callingOtherFunctions: [],
        calledByOtherFunctions: [],
      };
      // 处理函数调用其它函数的信息
      const callingFuncs = await FunctionRelation.find({ fromFuncId: functionId }).lean();
      const functionCallexpr = functionCfgCallexprMap.get(functionId.toString());
      if (!utility.isEmpty(functionCallexpr)) {
        callingFuncs.forEach((callingFunc) => {
          const newCallingFunc = {
            name: callingFunc.toFuncName,
            filePath: this.filePathMap.get(callingFunc.toFuncFileId.toString()),
            lines: '',
          };
          const callLines = new Set();
          functionCallexpr.forEach((expr) => {
            if (expr.mangled && expr.mangled === callingFunc.toFuncMangledId) {
              callLines.add(expr.begin.line);
            }
          });
          if (callLines.size > 0) {
            newCallingFunc.lines = Array.from(callLines).toString();
          }
          newFunc.callingOtherFunctions.push(newCallingFunc);
        });
      }
      // 处理该函数被其它函数调用的信息
      const calledByOtherFuncs = await FunctionRelation.find({ toFuncId: functionId }).lean();
      await utility.arrayChunkOperation(calledByOtherFuncs, 10, async (calledFunc) => {
        const fromFuncId = calledFunc.fromFuncId.toString();
        const calledFunctionCfgCallexpr = functionCfgCallexprMap.get(fromFuncId);
        if (utility.isEmpty(calledFunctionCfgCallexpr) || !this.versionFunctionMap.has(fromFuncId)) {
          return;
        }
        const newCalledFunc = {
          name: this.versionFunctionMap.get(fromFuncId).functionName,
          filePath: this.filePathMap.get(calledFunc.fromFuncFileId.toString()),
          lines: '',
        };
        const calledLines = new Set();
        calledFunctionCfgCallexpr.forEach((expr) => {
          if (expr.mangled && expr.mangled === func.mangledId) {
            calledLines.add(expr.begin.line);
          }
        });
        if (calledLines.size > 0) {
          newCalledFunc.lines = Array.from(calledLines).toString();
        }
        newFunc.calledByOtherFunctions.push(newCalledFunc);
      });
      newFunc.callingOtherFunctions.sort((a, b) => a.line - b.line);
      newFunc.calledByOtherFunctions.sort((a, b) => a.line - b.line);
      resFileFunctions.push(newFunc);
    });
    resFileFunctions.sort((a, b) => a.line - b.line);
    return resFileFunctions;
  }

  /**
   * 批量处理文件
   * @param {Array} files 文件列表
   * @param {String} versionDir 版本目录
   * @return {Promise}
   */
  async handleFiles(files, versionDir, progressIndicator) {
    if (!files && files.length === 0) {
      return Promise.resolve();
    }
    const compiledFiles = [];
    let number = 0;
    files.forEach((file) => {
      if (file.status && file.status === statusKind.compiled && file.kind === kind.file) {
        number += 1;
        this.filePathMap.set(file._id.toString(), file.path);
        compiledFiles.push({
          fileData: file,
          number,
        });
      }
    });
    return utility.arrayChunkOperation(compiledFiles, 1, async (file) => {
      const fileSummary = await this.handleFile(file.fileData, versionDir);
      progressIndicator.advance(1 / compiledFiles.length);
      return fileSummary;
    });
  }

  /**
   * 处理单个文件
   * @param {Object} file 单个文件信息
   * @param {Array} funcInvokes 函数调用情况
   * @param {String} versionDir 版本目录
   * @param {Boolean} copyStatic 复制静态资源
   * @returns {Promise}
   *
   check:
          DD（两次变量定义之间没有被使用），DU（变量定义但没有使用），UR（变量未定义就引用）
          RO(只引用) DO(只定义) DR(既定义也使用)
          R(引用) D(定义) E(声明) I(作为输入) O(作为输出)
   kind:
        L(局部变量) G(全局变量) P(参数) LG(局部全局变量/局部静态变量)
   *
   */
  async handleFile(file, versionDir) {
    const reportPath = `${path.join(versionDir, path.dirname(file.path), file.fileName)}`;
    await fs.ensureDir(path.dirname(reportPath));
    const fileSummary = {
      headerRightContent: file.fileName,
      reportName: file.fileName,
      fileName: file.fileName,
      filePath: file.path,
      functions: [],
      UR: [], // 局部变量未定义就引用（未初始化）
      DU: [], // 局部变量定义了但没有使用（在其作用域中没有使用）
      DD: [], // 局部变量两次定义之间没有被使用
      globalsRefOnly: [], // 只引用（全局变量的值在函数中被引用但没有被改变）
      globalsDefOnly: [], // 只定义（全局变量在函数中只被赋值，但在函数中没有被引用）
      globalsDefRefMod: [], // 全局变量既被定义也被引用和改变
      variables: [], // 所有分析的变量
    };
    const fileContent = await getLinesOfRemoteFileStream(file._id, { start: 1 });
    // 构建文件报告结构数据
    const fileDataFlows = await FileDataFlow.find({ fileId: file._id }, ['check', 'var', 'range', 'kind', 'type', 'function']).lean();
    const urMap = new Map();
    const duMap = new Map();
    const ddMap = new Map();
    const globalsRefOnlyMap = new Map();
    const globalsDefOnlyMap = new Map();
    const globalsDefRefModMap = new Map();
    const variablesMap = new Map();
    fileDataFlows.forEach(({
      check, var: variableName, range, kind, function: functionName, type: typeName,
    }) => {
      if (!range || !kind || !check) {
        return;
      }
      const { begin, end } = range;
      const beginLine = begin && begin.line ? begin.line : '';
      const endLine = end && end.line ? end.line : '';
      if (!beginLine || !endLine) {
        return;
      }
      const { content: bContent, line: bLine } = fileContent[parseInt(beginLine, 10) - 1];
      const { content: eContent, line: eLine } = fileContent[parseInt(endLine, 10) - 1];
      if (variableKind.local === kind) {
        switch (check) {
          case variableCheck.referencedBeforeDefined: {
            if (!urMap.has(variableName)) {
              urMap.set(variableName, {
                name: variableName,
                declarationL: [],
                referenceL: [],
                lines: [],
              });
            }
            const obj = urMap.get(variableName);
            obj.declarationL.push(beginLine);
            obj.lines.push({ content: bContent.trim(), line: bLine });
            obj.referenceL.push(endLine);
            if (beginLine !== endLine) {
              obj.lines.push({ content: eContent.trim(), line: eLine });
            }
            urMap.set(variableName, obj);
            break;
          }
          case variableCheck.definedButNotUsed: {
            if (!duMap.has(variableName)) {
              duMap.set(variableName, {
                name: variableName,
                definitionL: [],
                terminationL: [],
                lines: [],
              });
            }
            const obj = duMap.get(variableName);
            obj.definitionL.push(beginLine);
            obj.lines.push({ content: bContent.trim(), line: bLine });
            obj.terminationL.push(endLine);
            if (beginLine !== endLine) {
              obj.lines.push({ content: eContent.trim(), line: eLine });
            }
            duMap.set(variableName, obj);
            break;
          }
          case variableCheck.notUsedBetweenTwoDefined: {
            if (!ddMap.has(variableName)) {
              ddMap.set(variableName, {
                name: variableName,
                definitionL: [],
                redefineL: [],
                lines: [],
              });
            }
            const obj = ddMap.get(variableName);
            obj.definitionL.push(beginLine);
            obj.lines.push({ content: bContent.trim(), line: bLine });
            if (beginLine !== endLine) {
              obj.lines.push({ content: eContent.trim(), line: eLine });
              obj.redefineL.push(endLine);
            }
            ddMap.set(variableName, obj);
            break;
          }
          default: break;
        }
      }
      if (variableKind.global === kind) {
        switch (check) {
          case variableCheck.onlyReferenced: {
            if (!globalsRefOnlyMap.has(variableName)) {
              globalsRefOnlyMap.set(variableName, {
                name: variableName,
                referenceL: [],
                lines: [],
              });
            }
            const obj = globalsRefOnlyMap.get(variableName);
            obj.referenceL.push(beginLine);
            obj.lines.push({ content: bContent.trim(), line: bLine });
            if (beginLine !== endLine) {
              obj.lines.push({ content: eContent.trim(), line: eLine });
              obj.referenceL.push(endLine);
            }
            globalsRefOnlyMap.set(variableName, obj);
            break;
          }
          case variableCheck.onlyDefined: {
            if (!globalsDefOnlyMap.has(variableName)) {
              globalsDefOnlyMap.set(variableName, {
                name: variableName,
                definitionL: [],
                lines: [],
              });
            }
            const obj = globalsDefOnlyMap.get(variableName);
            obj.definitionL.push(beginLine);
            obj.lines.push({ content: bContent.trim(), line: bLine });
            if (beginLine !== endLine) {
              obj.lines.push({ content: eContent.trim(), line: eLine });
              obj.definitionL.push(endLine);
            }
            globalsDefOnlyMap.set(variableName, obj);
            break;
          }
          case variableCheck.bothDefineAndUsed: {
            if (!globalsDefRefModMap.has(variableName)) {
              globalsDefRefModMap.set(variableName, {
                name: variableName,
                defRefModL: [],
                lines: [],
              });
            }
            const obj = globalsDefRefModMap.get(variableName);
            obj.defRefModL.push(beginLine);
            obj.lines.push({ content: bContent.trim(), line: bLine });
            if (beginLine !== endLine) {
              obj.lines.push({ content: eContent.trim(), line: eLine });
              obj.defRefModL.push(endLine);
            }
            globalsDefRefModMap.set(variableName, obj);
            break;
          }
          default: break;
        }
      }
      const key = `${variableName}-${beginLine}-${check}`;
      if (!variablesMap.has(key)) {
        variablesMap.set(key, {
          name: variableName,
          functionName: functionName || '',
          categoryName: '',
          categoryKind: kind,
          type: typeName || '',
          scope: '',
          locations: [],
        });
      }
      let scopeName;
      let categoryName;
      switch (kind) {
        case variableKind.local: {
          categoryName = this.i18n.__('tests.localVariables');
          break;
        }
        case variableKind.global: {
          categoryName = this.i18n.__('tests.global');
          break;
        }
        case variableKind.parameter: {
          categoryName = this.i18n.__('tests.variables');
          break;
        }
        case variableKind.localStatic: {
          categoryName = this.i18n.__('tests.statics');
          break;
        }
        default: break;
      }
      switch (check) {
        case variableCheck.asInput: {
          scopeName = this.i18n.__('tests.variableScope.asInput');
          break;
        }
        case variableCheck.asOutput: {
          scopeName = this.i18n.__('tests.variableScope.asOutpout');
          break;
        }
        case variableCheck.wasDeclared: {
          scopeName = this.i18n.__('tests.variableScope.wasDeclared');
          break;
        }
        case variableCheck.onlyDefined:
          scopeName = this.i18n.__('tests.variableScope.onlyDefined');
          break;
        case variableCheck.notUsedBetweenTwoDefined:
          scopeName = this.i18n.__('tests.variableScope.notUsedBetweenTwoDefined');
          break;
        case variableCheck.definedButNotUsed:
          scopeName = this.i18n.__('tests.variableScope.definedButNotUsed');
          break;
        case variableCheck.bothDefineAndUsed:
          scopeName = this.i18n.__('tests.variableScope.bothDefineAndUsed');
          break;
        case variableCheck.defined: {
          scopeName = this.i18n.__('tests.variableScope.defined');
          break;
        }
        case variableCheck.onlyReferenced:
          scopeName = this.i18n.__('tests.variableScope.onlyReferenced');
          break;
        case variableCheck.referencedBeforeDefined:
          scopeName = this.i18n.__('tests.variableScope.referencedBeforeDefined');
          break;
        case variableCheck.referenced: {
          scopeName = this.i18n.__('tests.variableScope.referenced');
          break;
        }
        default: break;
      }
      variablesMap.get(key).scope = scopeName;
      variablesMap.get(key).categoryName = categoryName;
      if (!variablesMap.get(key).locations.includes(beginLine)) {
        variablesMap.get(key).locations.push(beginLine);
      }
    });
    function toSort(array, key) {
      if (key) {
        return array.sort((a, b) => (a[key] >= b[key] ? 1 : -1));
      }
      return Array.from(new Set(array)).sort((a, b) => (a >= b ? 1 : -1));
    }
    [
      { key: 'UR', value: urMap },
      { key: 'DU', value: duMap },
      { key: 'DD', value: ddMap },
      { key: 'globalsRefOnly', value: globalsRefOnlyMap },
      { key: 'globalsDefOnly', value: globalsDefOnlyMap },
      { key: 'globalsDefRefMod', value: globalsDefRefModMap },
      { key: 'variables', value: variablesMap },
    ].forEach(({ key, value }) => {
      value.forEach((childValue) => {
        if (childValue.lines) {
          childValue.lines = toSort(childValue.lines, 'line');
        }
        if (childValue.declarationL) {
          childValue.declarationL = toSort(childValue.declarationL).toString();
        }
        if (childValue.referenceL) {
          childValue.referenceL = toSort(childValue.referenceL).toString();
        }
        if (childValue.definitionL) {
          childValue.definitionL = toSort(childValue.definitionL).toString();
        }
        if (childValue.terminationL) {
          childValue.terminationL = toSort(childValue.terminationL).toString();
        }
        if (childValue.redefineL) {
          childValue.redefineL = toSort(childValue.redefineL).toString();
        }
        if (childValue.defRefModL) {
          childValue.defRefModL = toSort(childValue.defRefModL).toString();
        }
        fileSummary[key].push(childValue);
      });
    });
    if (fileSummary.variables) {
      fileSummary.variables = toSort(fileSummary.variables, 'name');
    }
    fileSummary.functions = await this.handleFunctionsOfFile(file);
    await this.generateFileReport(fileSummary, reportPath);
    return fileSummary;
  }

  async generateSummaryReport(data, output) {
    const template = 'summary-template';
    return this.generateReport(template, data, output, { copyStatic: true });
  }

  /**
  * 生成summary报告
  * @param {Array[Object]} filesData 分析的文件的数据
  * @param {number} progressIndictor 进度器
  * @returns
  */
  async generateSummary(filesData, progressIndictor) {
    const summaryData = {
      reportName: this.reportName,
      projectName: this.project.projectName,
      versionName: this.version.versionName,
      reportTime: moment().format('YYYY/MM/DD HH:mm:ss'),
      reportFooterText: this.reportFooterText,
      company: this.i18n.__('tests.company'),
      files: filesData,
      globalVariables: [],
    };
    const output = `${path.join(this.versionDir, 'summary')}`;
    await this.generateSummaryReport(summaryData, output);
    progressIndictor.finish();
  }
}
module.exports = DataStreamReporter;
